package com.ty.user.starter.aspect;

import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.ty.user.starter.annon.TyLog;
import com.ty.user.starter.entity.Log;
import com.ty.user.starter.service.LogService;
import com.ty.user.starter.util.ShiroUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;

/**
 * 系统日志切面
 *
 * @author minku
 * @date 2021-10-21
 */
@Slf4j
@Aspect
@Component
public class LogAspect {

    private final ThreadLocal<Log> sysLogThreadLocal = new ThreadLocal<>();

    private final Executor userMgtThreadPoolTaskExecutor;
    private final LogService logService;

    @Autowired
    public LogAspect(Executor userMgtThreadPoolTaskExecutor, LogService logService) {
        this.userMgtThreadPoolTaskExecutor = userMgtThreadPoolTaskExecutor;
        this.logService = logService;
    }

    /**
     * 日志切点
     */
    @Pointcut("@annotation(com.ty.user.starter.annon.TyLog)")
    public void sysLogAspect() {
    }

    /**
     * 前置通知
     */
    @Before(value = "sysLogAspect()")
    public void doBefore(JoinPoint joinPoint) {
        HttpServletRequest request = ((ServletRequestAttributes) Objects
                .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        // 从切面织入点处通过反射机制获取织入点处的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取切入点所在的方法
        Method method = signature.getMethod();
        TyLog tyLog = method.getAnnotation(TyLog.class);

        Log sysLog = new Log();
        // 操作说明
        sysLog.setModule(tyLog.module());
        sysLog.setTypeEnum(tyLog.typeEnum());
        sysLog.setDescription(tyLog.description());

        // 操作人信息
        if (ShiroUtils.getUser() != null) {
            sysLog.setCreateUserId(ShiroUtils.getUser().getId());
            sysLog.setCreateUserName(ShiroUtils.getUser().getUsername());
        }

        sysLog.setStartTime(LocalDateTime.now());
        sysLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));
        sysLog.setRequestParams(formatParams(request.getParameterMap()));
        sysLog.setRequestMethod(request.getMethod());
        sysLog.setRequestIp(ServletUtil.getClientIP(request));
        sysLog.setServerAddress(request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort());
        String userAgentStr = request.getHeader("User-Agent");
        sysLog.setUserAgent(userAgentStr);
        UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
        sysLog.setDeviceName(userAgent.getOs().getName());
        sysLog.setBrowserName(userAgent.getBrowser().getName());

        sysLogThreadLocal.set(sysLog);

        log.info("开始计时: {}  URI: {}  IP: {}", sysLog.getStartTime(), sysLog.getRequestUri(), sysLog.getRequestIp());
    }

    /**
     * 返回通知
     */
    @AfterReturning(pointcut = "sysLogAspect()", returning = "ret")
    public void doAfterReturning(Object ret) {
        Log sysLog = sysLogThreadLocal.get();
        sysLog.setEndTime(LocalDateTime.now());
        sysLog.setExecuteTime(ChronoUnit.MILLIS.between(sysLog.getStartTime(), sysLog.getEndTime()));
        userMgtThreadPoolTaskExecutor.execute(new SaveLogThread(sysLog, logService));
        sysLogThreadLocal.remove();
        log.info("计时结束: {}  用时: {}ms  URI: {}", sysLog.getEndTime(), sysLog.getExecuteTime(), sysLog.getRequestUri());
    }

    /**
     * 异常通知,注意
     * 由于配置了全局异常处理类{@link com.ty.user.starter.exception.CommonExceptionAdvice},因此此注解不生效
     */
    @AfterThrowing(pointcut = "sysLogAspect()", throwing = "e")
    public void doAfterThrowable(Throwable e) {
        Log sysLog = sysLogThreadLocal.get();
        sysLog.setEndTime(LocalDateTime.now());
        sysLog.setExecuteTime(ChronoUnit.MINUTES.between(sysLog.getStartTime(), sysLog.getEndTime()));
        userMgtThreadPoolTaskExecutor.execute(new SaveLogThread(sysLog, logService));
        sysLogThreadLocal.remove();

        log.info("计时结束: {}  用时: {}ms  URI: {}", sysLog.getEndTime(), sysLog.getExecuteTime(), sysLog.getRequestUri());
    }

    /**
     * 格式化参数
     *
     * @param parameterMap 参数
     * @return 参数字符串格式化
     */
    private String formatParams(Map<String, String[]> parameterMap) {
        if (parameterMap == null) {
            return null;
        }
        StringBuilder params = new StringBuilder();
        for (Map.Entry<String, String[]> param : (parameterMap).entrySet()) {
            if (params.length() != 0) {
                params.append("&");
            }
            params.append(param.getKey()).append("=");
            if (CharSequenceUtil.endWithIgnoreCase(param.getKey(), "password")) {
                params.append("*");
            } else if (param.getValue() != null) {
                params.append(ArrayUtil.join(param.getValue(), ","));
            }
        }
        return params.toString();
    }

    /**
     * 保存日志线程
     */
    private static class SaveLogThread implements Runnable {
        private final Log sysLog;
        private final LogService logService;

        public SaveLogThread(Log sysLog, LogService logService) {
            this.sysLog = sysLog;
            this.logService = logService;
        }

        @Override
        public void run() {
            sysLog.setCreateDate(LocalDateTime.now());
            logService.save(sysLog);
        }
    }
}